package robot;

import static robot.ExecutionRobot.Kernel32.*;
import static robot.ExecutionRobot.Psapi.*;
import static robot.ExecutionRobot.User32.*;
import logging.*;
import settings.*;

import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.platform.win32.WinDef.HWND;
import com.sun.jna.platform.win32.WinUser;
import com.sun.jna.ptr.PointerByReference;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.List;
import javax.imageio.*;

/**
 * Utility class for simulating user input.
 * @author BDS
 */
public class ExecutionRobot extends Robot
{
	private final int MAX_TITLE_LENGTH = 1024;
	
	private Process runningEnvironment;
	private Integer currentIteration;
	
	/**
	 * Class constructor.
	 * @throws AWTException Caused by instantiation of inherited Robot class
	 */
	public ExecutionRobot() throws AWTException
	{
		super();
		setAutoDelay(5000);
		
		currentIteration = 0;
	}
	
	/**
	 * Getter for the running Environment's Process.
	 * @return The running Environment's Process.
	 */
	public Process getRunningEnvironment() { return runningEnvironment; }
	
	/**
	 * Getter for the current Workflow iteration number.
	 * @return The current Workflow iteration number.
	 */
	public Integer getCurrentIteration() { return currentIteration; }
	
	/**
	 * Setter for the current Workflow iteration number.
	 * @param currentIteration The current Workflow iteration number.
	 */
	public void setCurrentIteration(Integer currentIteration) { this.currentIteration = currentIteration; }
	
	/**
	 * Loads the Environment by running the Environment's path.
	 * @param environment The Environment to load.
	 * @return True if successful; false otherwise.
	 */
	public boolean loadEnvironment(Environment environment)
	{
		boolean success = true;
		
		try
		{
			Logger.logMessage(environment.getName() + ": Starting");
			
			// Open the application.
			runningEnvironment = Runtime.getRuntime().exec("\"" + environment.getPath() + "\" " + environment.getArgument1() + " " + environment.getArgument2());
			
			// Prevent any unexpected behavior by defaulting the mouse location.
			mouseMove(0, 0);
		}
		catch (Exception exception)
		{
			Logger.logException(exception);
			success = false;
		}
		
		return success;
	}

	/**
	 * Clicks the Mouse1 button.
	 */
	public void clickMouse()
	{
		mousePress(InputEvent.BUTTON1_MASK);
		mouseRelease(InputEvent.BUTTON1_MASK);
	}

	/**
	 * Presses and releases a key.
	 * @param keycode The key to press.
	 */
	public void keyStroke(int keycode)
	{
		keyPress(keycode);
		keyRelease(keycode);
	}

	/**
	 * Simultaneously presses and releases many keys.
	 * @param keycodes The keys to press.
	 */
	public void keyStroke(Integer[] keycodes)
	{
		for (Integer keycode : keycodes)
		{
			if (keycode != null)
			{
				keyPress(keycode);
			}
		}
		for (Integer keycode : keycodes)
		{
			if (keycode != null)
			{
				keyRelease(keycode);
			}
		}
	}
	
	/**
	 * Types the given String.
	 * @param type The String to type.
	 */
	public void type(String type) {
		
		boolean hotkeyActive = false;
		for (char character : type.toCharArray())
		{
			if (hotkeyActive)
			{
				switch (character) {
		        case 'D': 
		        	type(new SimpleDateFormat("MM/dd/yyy").format(new Date()));
		        	break;
		        case 'T': 
		        	type(new SimpleDateFormat("HH:mm:ss").format(new Date()));
		        	break;
		        case 'I': 
		        	type(currentIteration.toString());
		        	break;
		        default:
		        	type('%');
		        	type(character);
		        	break;
				}
				
				hotkeyActive = false;
			}
			else
			{
				if (character == '%') 
				{
					hotkeyActive = true;
				}
				else
				{
					type(character);
				}
			}
		}
    }
	
	/**
	 * Types the given char.
	 * @param character The char to type.
	 */
	public void type(char character)
	{
		switch (character) 
        {
        case 'a': keyStroke(KeyEvent.VK_A); break;
        case 'b': keyStroke(KeyEvent.VK_B); break;
        case 'c': keyStroke(KeyEvent.VK_C); break;
        case 'd': keyStroke(KeyEvent.VK_D); break;
        case 'e': keyStroke(KeyEvent.VK_E); break;
        case 'f': keyStroke(KeyEvent.VK_F); break;
        case 'g': keyStroke(KeyEvent.VK_G); break;
        case 'h': keyStroke(KeyEvent.VK_H); break;
        case 'i': keyStroke(KeyEvent.VK_I); break;
        case 'j': keyStroke(KeyEvent.VK_J); break;
        case 'k': keyStroke(KeyEvent.VK_K); break;
        case 'l': keyStroke(KeyEvent.VK_L); break;
        case 'm': keyStroke(KeyEvent.VK_M); break;
        case 'n': keyStroke(KeyEvent.VK_N); break;
        case 'o': keyStroke(KeyEvent.VK_O); break;
        case 'p': keyStroke(KeyEvent.VK_P); break;
        case 'q': keyStroke(KeyEvent.VK_Q); break;
        case 'r': keyStroke(KeyEvent.VK_R); break;
        case 's': keyStroke(KeyEvent.VK_S); break;
        case 't': keyStroke(KeyEvent.VK_T); break;
        case 'u': keyStroke(KeyEvent.VK_U); break;
        case 'v': keyStroke(KeyEvent.VK_V); break;
        case 'w': keyStroke(KeyEvent.VK_W); break;
        case 'x': keyStroke(KeyEvent.VK_X); break;
        case 'y': keyStroke(KeyEvent.VK_Y); break;
        case 'z': keyStroke(KeyEvent.VK_Z); break;
        case 'A': keyStroke(new Integer[] { KeyEvent.VK_SHIFT, KeyEvent.VK_A }); break;
        case 'B': keyStroke(new Integer[] { KeyEvent.VK_SHIFT, KeyEvent.VK_B }); break;
        case 'C': keyStroke(new Integer[] { KeyEvent.VK_SHIFT, KeyEvent.VK_C }); break;
        case 'D': keyStroke(new Integer[] { KeyEvent.VK_SHIFT, KeyEvent.VK_D }); break;
        case 'E': keyStroke(new Integer[] { KeyEvent.VK_SHIFT, KeyEvent.VK_E }); break;
        case 'F': keyStroke(new Integer[] { KeyEvent.VK_SHIFT, KeyEvent.VK_F }); break;
        case 'G': keyStroke(new Integer[] { KeyEvent.VK_SHIFT, KeyEvent.VK_G }); break;
        case 'H': keyStroke(new Integer[] { KeyEvent.VK_SHIFT, KeyEvent.VK_H }); break;
        case 'I': keyStroke(new Integer[] { KeyEvent.VK_SHIFT, KeyEvent.VK_I }); break;
        case 'J': keyStroke(new Integer[] { KeyEvent.VK_SHIFT, KeyEvent.VK_J }); break;
        case 'K': keyStroke(new Integer[] { KeyEvent.VK_SHIFT, KeyEvent.VK_K }); break;
        case 'L': keyStroke(new Integer[] { KeyEvent.VK_SHIFT, KeyEvent.VK_L }); break;
        case 'M': keyStroke(new Integer[] { KeyEvent.VK_SHIFT, KeyEvent.VK_M }); break;
        case 'N': keyStroke(new Integer[] { KeyEvent.VK_SHIFT, KeyEvent.VK_N }); break;
        case 'O': keyStroke(new Integer[] { KeyEvent.VK_SHIFT, KeyEvent.VK_O }); break;
        case 'P': keyStroke(new Integer[] { KeyEvent.VK_SHIFT, KeyEvent.VK_P }); break;
        case 'Q': keyStroke(new Integer[] { KeyEvent.VK_SHIFT, KeyEvent.VK_Q }); break;
        case 'R': keyStroke(new Integer[] { KeyEvent.VK_SHIFT, KeyEvent.VK_R }); break;
        case 'S': keyStroke(new Integer[] { KeyEvent.VK_SHIFT, KeyEvent.VK_S }); break;
        case 'T': keyStroke(new Integer[] { KeyEvent.VK_SHIFT, KeyEvent.VK_T }); break;
        case 'U': keyStroke(new Integer[] { KeyEvent.VK_SHIFT, KeyEvent.VK_U }); break;
        case 'V': keyStroke(new Integer[] { KeyEvent.VK_SHIFT, KeyEvent.VK_V }); break;
        case 'W': keyStroke(new Integer[] { KeyEvent.VK_SHIFT, KeyEvent.VK_W }); break;
        case 'X': keyStroke(new Integer[] { KeyEvent.VK_SHIFT, KeyEvent.VK_X }); break;
        case 'Y': keyStroke(new Integer[] { KeyEvent.VK_SHIFT, KeyEvent.VK_Y }); break;
        case 'Z': keyStroke(new Integer[] { KeyEvent.VK_SHIFT, KeyEvent.VK_Z }); break;
        case '`': keyStroke(KeyEvent.VK_BACK_QUOTE); break;
        case '0': keyStroke(KeyEvent.VK_0); break;
        case '1': keyStroke(KeyEvent.VK_1); break;
        case '2': keyStroke(KeyEvent.VK_2); break;
        case '3': keyStroke(KeyEvent.VK_3); break;
        case '4': keyStroke(KeyEvent.VK_4); break;
        case '5': keyStroke(KeyEvent.VK_5); break;
        case '6': keyStroke(KeyEvent.VK_6); break;
        case '7': keyStroke(KeyEvent.VK_7); break;
        case '8': keyStroke(KeyEvent.VK_8); break;
        case '9': keyStroke(KeyEvent.VK_9); break;
        case '-': keyStroke(KeyEvent.VK_MINUS); break;
        case '=': keyStroke(KeyEvent.VK_EQUALS); break;
        case '~': keyStroke(new Integer[] { KeyEvent.VK_SHIFT, KeyEvent.VK_BACK_QUOTE }); break;
        case '!': keyStroke(KeyEvent.VK_EXCLAMATION_MARK); break;
        case '@': keyStroke(KeyEvent.VK_AT); break;
        case '#': keyStroke(KeyEvent.VK_NUMBER_SIGN); break;
        case '$': keyStroke(KeyEvent.VK_DOLLAR); break;
        case '%': keyStroke(new Integer[] { KeyEvent.VK_SHIFT, KeyEvent.VK_5 }); break;
        case '^': keyStroke(KeyEvent.VK_CIRCUMFLEX); break;
        case '&': keyStroke(KeyEvent.VK_AMPERSAND); break;
        case '*': keyStroke(KeyEvent.VK_ASTERISK); break;
        case '(': keyStroke(KeyEvent.VK_LEFT_PARENTHESIS); break;
        case ')': keyStroke(KeyEvent.VK_RIGHT_PARENTHESIS); break;
        case '_': keyStroke(KeyEvent.VK_UNDERSCORE); break;
        case '+': keyStroke(KeyEvent.VK_PLUS); break;
        case '\t': keyStroke(KeyEvent.VK_TAB); break;
        case '\n': keyStroke(KeyEvent.VK_ENTER); break;
        case '[': keyStroke(KeyEvent.VK_OPEN_BRACKET); break;
        case ']': keyStroke(KeyEvent.VK_CLOSE_BRACKET); break;
        case '\\': keyStroke(KeyEvent.VK_BACK_SLASH); break;
        case '{': keyStroke(new Integer[] { KeyEvent.VK_SHIFT, KeyEvent.VK_OPEN_BRACKET }); break;
        case '}': keyStroke(new Integer[] { KeyEvent.VK_SHIFT, KeyEvent.VK_CLOSE_BRACKET }); break;
        case '|': keyStroke(new Integer[] { KeyEvent.VK_SHIFT, KeyEvent.VK_BACK_SLASH }); break;
        case ';': keyStroke(KeyEvent.VK_SEMICOLON); break;
        case ':': keyStroke(KeyEvent.VK_COLON); break;
        case '\'': keyStroke(KeyEvent.VK_QUOTE); break;
        case '"': keyStroke(KeyEvent.VK_QUOTEDBL); break;
        case ',': keyStroke(KeyEvent.VK_COMMA); break;
        case '<': keyStroke(KeyEvent.VK_LESS); break;
        case '.': keyStroke(KeyEvent.VK_PERIOD); break;
        case '>': keyStroke(KeyEvent.VK_GREATER); break;
        case '/': keyStroke(KeyEvent.VK_SLASH); break;
        case '?': keyStroke(new Integer[] { KeyEvent.VK_SHIFT, KeyEvent.VK_SLASH }); break;
        case ' ': keyStroke(KeyEvent.VK_SPACE); break;
        }
	}
	
	/**
	 * Checks if the specified image is currently displayed.
	 * Optionally centers the mouse on the image, if displayed.
	 * @param imageName The name of the image to check with a path relative to the running directory's 'Images' folder.
	 * @param centerMouse Determines if the mouse is to be centered on the first image match, if displayed.
	 * @return True if the specified image was found on the screen; false otherwise.
	 */
	public boolean checkForImage(String imageName, boolean centerMouse)
	{
		boolean foundImage = false;
		
		try
		{	
			BufferedImage image = ImageIO.read(new File(System.getProperty("user.dir") + "\\Images\\" + imageName));
			PixelGrabber imagePixelGrabber = new PixelGrabber(image, 0, 0, -1, -1, false);
			
			Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
			BufferedImage screen = createScreenCapture(new Rectangle(0, 0, (int)screenSize.getWidth(), (int)screenSize.getHeight()));
			PixelGrabber screenPixelGrabber = new PixelGrabber(screen, 0, 0, -1, -1, false);
			
			if (imagePixelGrabber.grabPixels() && screenPixelGrabber.grabPixels())
			{
				int[] imageData = (int[])imagePixelGrabber.getPixels();
				int[] screenData = (int[])screenPixelGrabber.getPixels();

				// Find each potential starting screen location by matching the image's first line.
				List<Point> potentialMatches = new ArrayList<Point>();
				for (int currentScreenY = 0; currentScreenY < screenPixelGrabber.getHeight() - imagePixelGrabber.getHeight(); currentScreenY++)
				{
					for (int currentScreenX = 0; currentScreenX < screenPixelGrabber.getWidth() - imagePixelGrabber.getWidth(); currentScreenX++)
					{
						boolean potentialMatch = true;
						for (int currentImageX = 0; currentImageX < imagePixelGrabber.getWidth(); currentImageX++)
						{
							if (imageData[currentImageX] != screenData[(currentScreenY * screenPixelGrabber.getWidth()) + currentScreenX + currentImageX])
							{
								potentialMatch = false;
								break;
							}
						}
						
						if (potentialMatch)
						{
							potentialMatches.add(new Point(currentScreenX, currentScreenY));
						}
					}
				}
				
				// Validate each potential screen location.
				Point firstValidMatch = null;
				for (Point potentialMatch : potentialMatches)
				{
					boolean validMatch = true;
					imageMatch: for (int currentImageY = 1; currentImageY < imagePixelGrabber.getHeight(); currentImageY++)
					{
						for (int currentImageX = 0; currentImageX < imagePixelGrabber.getWidth(); currentImageX++)
						{
							if (imageData[(currentImageY * imagePixelGrabber.getWidth()) + currentImageX] != 
								screenData[(((int) potentialMatch.getY()) * screenPixelGrabber.getWidth()) + (currentImageY * screenPixelGrabber.getWidth()) + 
								           (((int) potentialMatch.getX()) + currentImageX)])
							{
								validMatch = false;
								break imageMatch;
							}
						}
					}
					
					if (validMatch)
					{
						foundImage = true;
						firstValidMatch = potentialMatch;
						break;
					}
				}
				
				if (centerMouse && firstValidMatch != null)
				{
					mouseMove(((int) firstValidMatch.getX()) + imagePixelGrabber.getWidth() / 2,
							  ((int) firstValidMatch.getY()) + imagePixelGrabber.getHeight() / 2);
				}
			}
		}
		catch (Exception exception)
		{
			Logger.logException(exception);
		}
		
		return foundImage;
	}
	
	/**
	 * Registers the number of visible instances of the provided image,
	 * up to a maximum of 10.
	 * @param image The image to scan for.
	 * @return The number of visible instances of the provided to a maximum of 10. 
	 * @throws InterruptedException Another thread has interrupted this thread.
	 */
	public int checkVisibleInstances(BufferedImage image) throws InterruptedException
	{
	    int visibleInstances = 0;
	    
	    PixelGrabber imagePixelGrabber = new PixelGrabber(image, 0, 0, -1, -1, false);
        
        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
        BufferedImage screen = createScreenCapture(new Rectangle(0, 0, (int)screenSize.getWidth(), (int)screenSize.getHeight()));
        PixelGrabber screenPixelGrabber = new PixelGrabber(screen, 0, 0, -1, -1, false);
        
        if (imagePixelGrabber.grabPixels() && screenPixelGrabber.grabPixels())
        {
            int[] imageData = (int[])imagePixelGrabber.getPixels();
            int[] screenData = (int[])screenPixelGrabber.getPixels();

            // Find each potential starting screen location by matching the image's first line.
            List<Point> potentialMatches = new ArrayList<Point>();
            for (int currentScreenY = 0; currentScreenY < screenPixelGrabber.getHeight() - imagePixelGrabber.getHeight(); currentScreenY++)
            {
                for (int currentScreenX = 0; currentScreenX < screenPixelGrabber.getWidth() - imagePixelGrabber.getWidth(); currentScreenX++)
                {
                    boolean potentialMatch = true;
                    for (int currentImageX = 0; currentImageX < imagePixelGrabber.getWidth(); currentImageX++)
                    {
                        if (imageData[currentImageX] != screenData[(currentScreenY * screenPixelGrabber.getWidth()) + currentScreenX + currentImageX])
                        {
                            potentialMatch = false;
                            break;
                        }
                    }
                    
                    if (potentialMatch)
                    {
                        potentialMatches.add(new Point(currentScreenX, currentScreenY));
                    }
                }
            }
            
            // Validate each potential screen location.
            for (Point potentialMatch : potentialMatches)
            {
                boolean validMatch = true;
                imageMatch: for (int currentImageY = 0; currentImageY < imagePixelGrabber.getHeight(); currentImageY++)
                {
                    for (int currentImageX = 0; currentImageX < imagePixelGrabber.getWidth(); currentImageX++)
                    {
                        if (imageData[(currentImageY * imagePixelGrabber.getWidth()) + currentImageX] != 
                            screenData[(((int) potentialMatch.getY()) * screenPixelGrabber.getWidth()) + (currentImageY * screenPixelGrabber.getWidth()) + 
                                       (((int) potentialMatch.getX()) + currentImageX)])
                        {
                            validMatch = false;
                            break imageMatch;
                        }
                    }
                }
                
                if (validMatch)
                {
                    visibleInstances++;
                    if (visibleInstances == 10)
                    {
                        break;
                    }
                }
            }
        }
        
	    return visibleInstances;
	}
	
	/**
	 * Deletes the given directory.
	 * @param file The directory to delete.
	 * @return <code>true</code> if successful; <code>false</code> otherwise.
	 */
    public boolean deleteDirectory(File file) 
    {
    	boolean success = false;
    	
        if (file.isDirectory()) 
        {
        	success = true;
        	
            for (File subFile : file.listFiles())
            {
                success &= deleteDirectory(subFile);
                if (!success) 
                {
                    break;
                }
            }
            
            if (success)
            {
            	success &= file.delete();
            }
        }
        else
        {
        	success = file.delete();
        }
    
        return success;
    }

	/**
	 * Obtains the process name of the current active window.
	 * @return The process name of the current active window.
	 */
	public String getActiveWindowProcess()
	{
		char[] buffer = new char[MAX_TITLE_LENGTH * 2];
		PointerByReference pointer = new PointerByReference();
	    GetWindowThreadProcessId(GetForegroundWindow(), pointer);
	    Pointer process = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, pointer.getValue());
	    GetModuleBaseNameW(process, null, buffer, MAX_TITLE_LENGTH);
		return Native.toString(buffer).trim();
	}
	
	/**
	 * Obtains the name of the current active window.
	 * @return The name of the current active window.
	 */
	public String getActiveWindowTitle()
	{
		 char[] buffer = new char[MAX_TITLE_LENGTH * 2];
		 GetWindowTextW(GetForegroundWindow(), buffer, MAX_TITLE_LENGTH);
		 return Native.toString(buffer).trim();
	}
	
	/**
	 * Obtains a List containing the unique names of all windows.
	 * @return A List containing the unique names of all windows.
	 */
	public List<String> getWindowTitles()
	{
		final HashSet<String> windowTitles = new HashSet<String>();
		
		EnumWindows(new WinUser.WNDENUMPROC() 
		{
			/**
			 * Obtains the title for the given window.
			 * @param handle The window handle.
			 * @param pointer The window pointer.
			 * @return <tt>true</tt> if successful; <tt>false</tt> otherwise.
			 */
	        @Override
	        public boolean callback(HWND handle, Pointer pointer) 
	        {
	        	char[] buffer = new char[MAX_TITLE_LENGTH * 2];
	            GetWindowTextW(handle, buffer, MAX_TITLE_LENGTH);
	            String title = Native.toString(buffer).trim();

	            // Exclude empty windows
	            if (title == null || title.isEmpty()) 
	            {
	               return true;
	            }
	            
	            windowTitles.add(title);
	            
	            return true;
	        }
		}, null);
		
		return new ArrayList<String>(windowTitles);
	}
	
	/**
	 * Generates a regex for the given wildcard statement.
	 * @param wildcard The wildcard statement to convert to a regex.
	 * @return A regex representative of the given wildcard statement.
	 */
    public String wildcardToRegex(String wildcard)
    {
        StringBuffer stringBuffer = new StringBuffer(wildcard.length());
        
        stringBuffer.append('^');
        for (char character : wildcard.toCharArray()) 
        {
            switch (character) 
            {
                case '*':
                	stringBuffer.append(".*");
                    break;
                case '?':
                	stringBuffer.append(".");
                    break;
                // Escape regex special characters.
                case '(': case ')': case '[': case ']': case '$':
                case '^': case '.': case '{': case '}': case '|':
                case '\\':
                	stringBuffer.append("\\");
                	stringBuffer.append(character);
                    break;
                default:
                	stringBuffer.append(character);
                    break;
            }
        }
        stringBuffer.append('$');
        
        return(stringBuffer.toString());
    }

	/**
	 * Class wrapper for the Kernel32 assembly.
	 * @author BDS
	 */
	static class Kernel32
	{
	    static { Native.register("kernel32"); }
	    public static int PROCESS_QUERY_INFORMATION = 0x0400;
	    public static int PROCESS_VM_READ = 0x0010;
	    public static native int GetLastError();
	    public static native Pointer OpenProcess(int dwDesiredAccess, boolean bInheritHandle, Pointer pointer);
	}
	
	/**
	 * Class wrapper for the PSAPI assembly.
	 * @author BDS
	 */
	static class Psapi
	{
	    static { Native.register("psapi"); }
	    public static native int GetModuleBaseNameW(Pointer hProcess, Pointer hmodule, char[] lpBaseName, int size);
	}

	/**
	 * Class wrapper for the User32 assembly.
	 * @author BDS
	 */
	static class User32
	{
	    static { Native.register("user32"); }
	    public static native boolean EnumWindows(WinUser.WNDENUMPROC lpEnumFunc, Pointer arg);
	    public static native int GetWindowThreadProcessId(HWND hWnd, PointerByReference pref);
	    public static native HWND GetForegroundWindow();
	    public static native int GetWindowTextW(HWND hWnd, char[] lpString, int nMaxCount);
	}
}
